package dungeon;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import condition.Condition;
import condition.ConditionDefault;

import square.Direction;
import square.Square;
import square.TeleportationSquare;
import square.Rock;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Model;
import be.kuleuven.cs.som.annotate.Raw;
import border.Door;
/**
 * A class for dealing with leaf dungeons involving unidirectional associations
 * with squares and positions.
 * 
 * @invar	No more than 20% of the squares in this leaf dungeon may be slippery squares.
 * 			| getNbSlipperySquares() = getNbSquares() *0.2
 * 
 * @param	<E>
 * 			The type of the squares in this dungeon, E extends the Square type.
 * 
 * @version	2.0
 * @author Inge Gielis <inge.gielis@student.kuleuven.be>
 * @author Annelies Ketelslegers <annelies.ketelslegers@student.kuleuven.be>
 */

public abstract class LeafDungeon<E extends Square> extends Dungeon<E> {
	
	/**
	 * Initialize this new leaf dungeon with 20 as maximal coordinate for each direction.
	 * 
	 * @effect	This new dungeon is initialized with 20 as value for each maximal coordinate.
	 * 			|this(20,20,20);
	 */
	public LeafDungeon(){
		this(20,20,20);
	}
	
	/**
	 * Initialize this new leaf dungeon with the given values as the maximal values for the coordinates
	 * of the positions of squares in this leaf dungeon.
	 * 
	 * @param 	maxX
	 * 			The maximum value for the x coordinate of a position in a leaf dungeon.
	 * @param 	maxY
	 * 			The maximum value for the Y coordinate of a position in a leaf dungeon.
	 * @param 	maxZ
	 * 			The maximum value for the Z coordinate of a position in a leaf dungeon.
	 * 
	 * @throws 	IllegalArgumentException
	 * 			The given values for the maximal coordinates must all be positive.
	 * 			| maxX <0 || maxY <0 || maxZ <0
	 */
	public LeafDungeon(long maxX, long maxY, long maxZ) throws IllegalArgumentException{
		super(maxX,maxY,maxZ);
	}
	
	/**
	 * Terminate this leaf dungeon.
	 * 
	 * @post	If this leaf dungeon is not already terminated, this leaf dungeon will be terminated.
	 * 			|if(! this.isTerminated())
	 * 			| 		then new.isTerminated()
	 * @post	This dungeon no longer references any squares.
	 * 			|getNbSquares() == 0
	 * @effect	If this leaf dungeon is not already terminated,
	 * 			each square associated with this leaf dungeon will be terminated.
	 * 			|if(! this.isTerminated())
	 * 			|	for(Position position: getSquaresAndPositions().keySet()){
	 *			|		getSquareAt(position).terminate();}
	 */
	@Override
	public void terminate(){
		if(! isTerminated()){
			for(Position position: squares.keySet()){
				getSquareAt(position).terminate();
			}
			this.isTerminated = true;
			squares.clear();
		}
	}
	
	/**
	 * Return the total number of squares in this leaf dungeon.
	 */
	@Override
	@Basic
	public int getNbSquares(){
		return squares.values().size();
	}

	/**
	 * Return the square associated with this leaf dungeon at the given position.
	 * 
	 * @return	The square at the given position.
	 * 			| getSquaresAndPositions().get(position)
	 * @return	If there is no square present at the given position, null is returned.
	 * 			| if(! hasSquareAt(position))
	 * 			|	then result == null
	 */
	@Override
	@Basic 
	@Raw
	public E getSquareAt(Position position) throws IllegalArgumentException{
		if(! canHaveAsPosition(position)){
			throw new IllegalArgumentException("Position is not valid.");
		}
		return squares.get(position);
	}

	/**
	 * Return the position at which the given square is registered as a 
	 * square in this leaf dungeon.
	 * 
	 * @return	The given square is registered as a square for this 
	 * 			dungeon at the resulting position.
	 * 			| getSquareAt(result) == square
	 */
	@Override
	@Raw
	public Position getPositionOfSquare(E square) throws IllegalArgumentException{
		for(Position position : squares.keySet())
			if(squares.get(position) == square)
				return position;
		throw new IllegalArgumentException("Given square is not a square for this leaf dungeon."); 
	}

	/**
	 * Return an iterator returning squares in this leaf
	 * dungeon.
	 */
	@Override
	public Iterator<E> getSquares(final Condition condition){
		return new Iterator<E>(){
			
			private Iterator<E> it;
			private E nextSquare;
			private Condition c;
	
			{
				it = squares.values().iterator();
				nextSquare = null;
				if(condition == null)
					c = new ConditionDefault();
				else{c=condition;}
			}
	
			@Override
			public boolean hasNext() {
				while(nextSquare == null && it.hasNext()){
					E squareToCheck = it.next();
					if(c.checkFor(squareToCheck) ){
						nextSquare = squareToCheck;
					}
				}
				return nextSquare != null;
			}
	
			@Override
			public E next() {
				if(nextSquare == null)
					hasNext();
				E result = nextSquare;
				nextSquare = null;
				return result;
			}
	
			@Override
			public void remove() throws UnsupportedOperationException{
				throw new UnsupportedOperationException();
			}
		};
	}

	/**
	 * Check whether this leaf dungeon has proper squares associated with 
	 * it.
	 * 
	 * @return	True if and only if this dungeon can have each of the 
	 * 			squares at its position in this dungeon as its squares, 
	 * 			false otherwise.
	 * 			| result == 
	 * 			|	for each square in getSquaresAndPositions()
	 * 			|		 canHaveAsSquareAt(square, getPositionOfSquare(square))
	 */
	@Override
	@Raw
	public boolean hasProperSquares(){
		for(E square : squares.values()){
			if(! canHaveAsSquareAt(square, getPositionOfSquare(square)))
				return false;
		}
		return true;
	}
	
	/**
	 * Check whether the given square is attached to this leaf dungeon.
	 * 
	 * @return 	True if and only if the square is attached to this dungeon.
	 * 			|result == getSquaresAndPositions().containsValue(square)
	 */
	@Override
	public boolean hasAsSquare(Square square){
		return (squares.containsValue(square));
	}
	
	/**
	 * Check whether there is a square at the given position in this leaf dungeon.
	 * 
	 * @return	True if and only if the given position is a valid position and
	 * 			this leaf dungeon has a square at the given position.
	 * 			| result == canHaveAsPosition(position)&& this.getSquaresAndPositions().containsKey(position)
	 */
	@Override
	@Basic
	public boolean hasSquareAt(Position position){
		
		return (canHaveAsPosition(position) && this.squares.containsKey(position));
	}

	/**
	 * Add the given square at the given position as a square for this leaf dungeon.
	 * 
	 * @post	All the squares that are in the same space as the given square will have the same temperature and humidity.
	 * 			| for each square in new.getSpace(square)
	 * 			|	new.getSquareAt(position).getTemperature().equals(square.getTemperature())
	 * 			| 	&& new.getSquareAt(position).getHumidity().equals(square.getHumidity())
	 * @effect	If the square or a square of the dungeon on a neighboring position has a border
	 * 			on the borderline of the two squares, the most closed border will be kept.
	 * 			|for(Direction direction : Direction.values()){
	 * 			| 	Position neighboringPosition = direction.goStepInDirection(position);
	 * 			|		if(neighboringPosition != null && hasSquareAt(neighboringPosition)){
	 *			|			if(square.getBorder(direction)!=null || getSquareAt(neighboringPosition).getBorder(direction.getOpposite())!=null){
	 *			|				borderConstruction(square, getSquareAt(neighboringPosition), direction);
	 *@effect	All rocks surrounding the new space will recalculate their
	 *			temperature.
	 *			|for rock in new.getSurroundingRocks(square)
	 *			|	rock.setTemperature()
	 * @throw 	IllegalArgumentException
	 * 			This dungeon cannot have the given square
	 * 			as a square associated with this dungeon at the given position.
	 * 			|(! canHaveAsSquareAt(square,position))
	 * @throws	IllegalArgumentException
	 * 			The given square a neighboring square 
	 * 			or the given square is already attached to this dungeon
	 * 			| hasAsSquare(square) ||  ! square.hasNoNeighbours()
	 * @throws	IllegalArgumentException
	 * 			The given square is slippery and it is not possible to add another slippery
	 * 			square to this dungeon (maximum amount of slippery squares is reached).
	 * 			| square.isSlippery() && ! canAddSlipperySquare()
	 */
	@Override
	public void addSquare(Position position, E square) throws IllegalArgumentException{
		if(! canHaveAsSquareAt(square, position))
			throw new IllegalArgumentException("Cannot have the given square at the given position.");
		if(hasAsSquare(square) ||! square.hasNoNeighbours())
			throw new IllegalArgumentException("Cannot add this square.");
		if(square.hasSlipperyFloorMaterial() && ! canAddSlipperySquare())
			throw new IllegalArgumentException("Only 20% of squares can be slippery squares.");
		HashSet<TeleportationSquare> newSpace = new HashSet<TeleportationSquare>();
		if(! (square instanceof Rock))
			newSpace.add((TeleportationSquare) square);
		for(Direction direction : Direction.values()){
			Position neighboringPosition = direction.goStepInDirection(position);
			if(neighboringPosition != null && hasSquareAt(neighboringPosition)){
				if(square.getBorder(direction)!=null || getSquareAt(neighboringPosition).getBorder(direction.getOpposite())!=null){
					borderConstruction(square, getSquareAt(neighboringPosition), direction);
					if (square.getBorder(direction) instanceof Door && !((Door) square.getBorder(direction)).isClosed() 
							&& ! newSpace.contains(getSquareAt(neighboringPosition)))
						newSpace = mergeSpaces(newSpace, getSpace(getSquareAt(neighboringPosition)));						
				}
				else if (! newSpace.contains(getSquareAt(neighboringPosition))){
					newSpace = mergeSpaces(newSpace, getSpace(getSquareAt(neighboringPosition)));
				}
			}
		}
		squares.put(position,square);
		if(square instanceof Rock)
			((Rock) square).setTemperature();
		else{
			for(Rock rock: getSurroundingRocks(square))
				rock.setTemperature();
		}
		
	}
	
	

	

	
	/**
	 * Remove the given square from the set of squares attached to this leaf dungeon.
	 * 
	 * 
	 * @effect	If this leaf dungeon has the given square as one of its squares,
	 * 			then the given square will be terminated and the given square will be 
	 * 			removed from the collection of squares in this leaf dungeon.  
	 * 			| if(hasAsSquare(square))
	 * 			|	then square.terminate()
	 */
	@Override
	public void removeSquare(E square){
		if( hasAsSquare(square))
			square.terminate();
			squares.remove(getPositionOfSquare(square));
	}
	
	/**
	 * Remove the square for this leaf dungeon at the given position.
	 * 
	 * @effect	If this leaf dungeon has a square attached to it 
	 * 			at the given position, the square at the given
	 * 			position is removed from this leaf dungeon. 	
	 * 			| if(hasAsSquareAt(position))
	 * 			| 	removeSquare(getSquareAt(position))
	 */
	@Override
	public void removeSquareAt(Position position){
		removeSquare(getSquareAt(position));
	}
	
	
	/**
	 * Return the number of slippery squares in this leaf dungeon.
	 * 
	 * @return	For each slippery square in this dungeon, 
	 * 			the amount of slippery squares is incremented with 1.
	 * 			| for each square in squares.getSquaresAndPositions().values()
	 * 			|	if(square.hasSlipperyFloorMaterial())
	 * 			|		#[square | square in getSquaresAndPositions().values()  /\ square.hasSlipperyFloormaterial] ++
	 */
	@Override
	@Model
	protected int getNbSlipperySquares() {
		int nbSlipperySquares = 1;
		for(E square :squares.values())
			if (square.hasSlipperyFloorMaterial())
				nbSlipperySquares ++;
		return nbSlipperySquares;
	}

	/**
	 * Check whether a slippery square can be added to this dungeon,
	 * satisfying the rule that no more than 20 % of all the squares
	 * in this dungeon can be slippery.
	 * 
	 * @return	True if and only if the number of slippery squares in the 
	 * 			dungeon after a new square is added, is smaller than the total number 
	 * 			of squares after that new square is added, divided by 5.
	 * 			| if(this.getNbSlipperySquares < (this.getNbSquares() + 1)/5)
	 * 			| 	then result == true
	 */
	private boolean canAddSlipperySquare(){
		if(getNbSlipperySquares() <= (this.getNbSquares() + 1)/5)
			return true;
		return false;
	}

	/**
	 * Return a map of squares at different positions in this leaf dungeon.
	 * 		Keys are the positions.
	 * 		Values are the squares at those positions.
	 */
	@Model
	private Map<Position, E> getSquaresAndPositions(){
		return new HashMap<Position, E>(squares);
	}
	
	/**
	 * Map to store the squares in this leaf dungeon, with their positions.
	 * 		Keys are the positions on which squares are located in this leaf dungeon,
	 * 		values are the squares on those positions in this leaf dungeon.
	 */
	private Map<Position, E> squares = new HashMap<Position, E>();

}
